fix(sandbox): give each Hermes sandbox its own OpenAI-compatible API port - #8577
fix(sandbox): give each Hermes sandbox its own OpenAI-compatible API port#8577laitingsheng wants to merge 26 commits into
Conversation
…port The API port was a fixed host-wide 8642, so a second Hermes sandbox on one host could never own an inference forward. A snapshot clone therefore received no inference forward and its gateway restart could not converge. Allocate the port per sandbox from 8642-8652, persist it on the registry row, and publish it to the sandbox through the create environment. The entrypoint re-publishes it as a root-owned read-only marker because a one-shot sandbox exec does not inherit the supervisor environment. Forward recovery now resolves manifest-declared ports against the sandbox's own ports instead of demanding the agent defaults a sibling sandbox already holds. Signed-off-by: Tinson Lai <tinsonl@nvidia.com>
The first pass left three host-side surfaces on the manifest default: the `sandbox agent` rejection message, the onboarding ready summary, and the docs. Each told an operator to forward a port their sandbox does not use. Relaunch also reached the allocator through the sandbox-create environment. A sandbox registered before the port became per-sandbox has no recorded value, so the allocator saw its own live forward holding the default and moved it, leaving the sandbox bound to one port while the host forwarded another. A registered sandbox without a port now keeps the default; only an unregistered sandbox takes a fresh allocation. Signed-off-by: Tinson Lai <tinsonl@nvidia.com>
…er-sandbox Signed-off-by: Tinson Lai <tinsonl@nvidia.com> # Conflicts: # ci/source-architecture-budget.json
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Enterprise Run ID: 📒 Files selected for processing (9)
🚧 Files skipped from review as they are similar to previous changes (9)
📝 WalkthroughWalkthroughHermes now allocates API ports per sandbox from ports 8642–8652. The selected port is stored, propagated to startup and forwarding, securely published, and used by recovery, cloning, and health checks. ChangesHermes API port allocation and propagation
Estimated code review effort: 4 (Complex) | ~60 minutes Possibly related PRs
Suggested reviewers: 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
🧪 Generate unit tests (beta)
Comment |
|
🌿 Preview your docs: https://nvidia-preview-pr-8577.docs.buildwithfern.com/nemoclaw |
There was a problem hiding this comment.
Actionable comments posted: 4
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/lib/onboard/dashboard-port.ts (1)
318-344: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick winReject explicit Hermes API/dashboard port overlaps before sandbox creation.
assertDashboardPortNotReservedrejects dashboard ports in8642–8652, butresolveOnboardHermesApiPortaccepts any validNEMOCLAW_HERMES_API_PORTwithout checking the resolved dashboard port. ThusNEMOCLAW_HERMES_API_PORT=18789can map both resources to one host port. Reject this equality and add regression tests for both cases.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/lib/onboard/dashboard-port.ts` around lines 318 - 344, Update resolveOnboardHermesApiPort to reject configurations where the resolved Hermes API port equals the resolved dashboard port, including explicit NEMOCLAW_HERMES_API_PORT values and automatically selected ports. Ensure the rejection occurs before sandbox creation and add regression tests covering both explicit overlap and overlap produced by resolution.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@agents/hermes/start.sh`:
- Around line 173-196: Use the shared Hermes API-port validity contract of
8642–8652 at all affected sites: update agents/hermes/start.sh lines 173-196,
agents/hermes/mcp-config-transaction.py lines 88-106,
agents/hermes/plugin/__init__.py lines 1035-1051,
scripts/managed-gateway-control.py lines 1125-1151, and scripts/install.sh lines
465-485 to reject values outside that inclusive range while preserving existing
defaults and error handling.
In `@docs/deployment/deploy-to-headless-server.mdx`:
- Line 292: Update all listed documentation examples to reflect dynamic Hermes
API-port allocation: in docs/deployment/deploy-to-headless-server.mdx lines
292-292, qualify 8642 as the first-sandbox default and revise adjacent curl
guidance; in docs/reference/commands.mdx lines 1216-1217, replace fixed-port
wording and update the Hermes agent example to use the port from openshell
forward list; in docs/reference/commands.mdx lines 1918-1919, use the selected
API port for the gateway-token example; and in
docs/reference/troubleshooting.mdx lines 3132-3133, qualify the 8642
troubleshooting path and update its health/client examples accordingly.
In `@docs/reference/troubleshooting.mdx`:
- Around line 3163-3167: Update the troubleshooting guidance around the final
destroy/re-onboard instruction to distinguish new second Hermes sandboxes from
existing pre-change sandboxes. State that new sandboxes receive the next free
API port automatically, while destroy and re-onboard is required only for an
existing sandbox with a registry row lacking hermesApiPort.
In `@src/lib/agent/onboard.ts`:
- Around line 559-577: The API dashboard branch that calls
printAdditionalForwardPorts must pass sandboxName so Hermes resolves its
registered sandbox-specific API port instead of the manifest default. Update
that kind === "api" call site, and add coverage for the Hermes dashboard.kind
=== "api" path verifying the sandbox-specific port is announced.
---
Outside diff comments:
In `@src/lib/onboard/dashboard-port.ts`:
- Around line 318-344: Update resolveOnboardHermesApiPort to reject
configurations where the resolved Hermes API port equals the resolved dashboard
port, including explicit NEMOCLAW_HERMES_API_PORT values and automatically
selected ports. Ensure the rejection occurs before sandbox creation and add
regression tests covering both explicit overlap and overlap produced by
resolution.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Enterprise
Run ID: dc739679-b1ab-436c-a70f-c20c3887833e
📒 Files selected for processing (37)
agents/hermes/mcp-config-transaction.pyagents/hermes/plugin/__init__.pyagents/hermes/start.shci/source-architecture-budget.jsondocs/deployment/deploy-to-headless-server.mdxdocs/deployment/set-up-mcp-bridge.mdxdocs/get-started/quickstart-hermes.mdxdocs/reference/commands.mdxdocs/reference/troubleshooting.mdxscripts/install.shscripts/managed-gateway-control.pysrc/commands/sandbox/agent.tssrc/lib/actions/sandbox/agent/passthrough-help.tssrc/lib/actions/sandbox/agent/passthrough.test.tssrc/lib/actions/sandbox/agent/passthrough.tssrc/lib/actions/sandbox/forward-recovery-declared-ports.test.tssrc/lib/actions/sandbox/forward-recovery.tssrc/lib/actions/sandbox/process-recovery.tssrc/lib/actions/sandbox/snapshot-restore-clone-ports.test.tssrc/lib/actions/sandbox/snapshot.tssrc/lib/agent/onboard.test.tssrc/lib/agent/onboard.tssrc/lib/core/ports.tssrc/lib/onboard/agent-dashboard-forward.tssrc/lib/onboard/dashboard-port.tssrc/lib/onboard/hermes-api-port.test.tssrc/lib/onboard/hermes-api-port.tssrc/lib/onboard/hermes-dashboard.tssrc/lib/onboard/managed-startup-profile.test.tssrc/lib/onboard/managed-startup/profile.tssrc/lib/onboard/preflight-ports.tssrc/lib/onboard/sandbox-create-launch.tssrc/lib/onboard/sandbox-registration.tssrc/lib/state/gateway-registry.tssrc/lib/state/registry.tssrc/lib/state/registry/types.tstest/hermes-mcp-reload-convergence.test.ts
PR Review Advisor — No blocking findings reportedAdvisor assessment: No blocking advisor findings reported Model lanes
Second-opinion terminology and E2E selections are advisory. Live E2E does not run automatically for pull requests. 3 semantic terminology decisionsTerminology decisions are advisory. They affect the assessment only when a separate finding identifies concrete semantic impact.
E2E guidanceAdvisory only. A maintainer can dispatch the default E2E suite against this exact revision. Recommended E2E: This automated review informs maintainers. Warnings and suggestions do not require a response. A maintainer decides whether to merge. |
|
Note GitHub couldn't provide a complete incremental comparison for this pull request, so CodeRabbit is performing a full review instead. This review may take a little longer. |
cv
left a comment
There was a problem hiding this comment.
agents/hermes/start.sh:3319 publishes the trusted Hermes API-port marker only on the root startup path. The supported managed non-root path exits through lines 3254-3284 without creating it. agents/hermes/mcp-config-transaction.py, agents/hermes/plugin/__init__.py, and scripts/managed-gateway-control.py then silently use port 8642. For a sandbox configured for port 8643, a sandbox user can bind 8642 and cause reload or lifecycle probes to accept the wrong listener. Publish a supervisor-owned, sandbox-user-unwritable binding before non-root helpers can run, or make those helpers fail closed when the binding is absent. Add an integration regression test that starts managed non-root Hermes on 8643, verifies the binding ownership and mode, verifies all helpers probe 8643, and proves a listener on 8642 cannot satisfy readiness.
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@agents/hermes/plugin/__init__.py`:
- Around line 1048-1051: Update the port-parsing logic around the raw marker
conversion to catch ValueError from int(raw) and return the existing 8642
fallback, including for non-ASCII digit strings accepted by isdigit(). Add a
negative-path test covering such input.
In `@src/lib/onboard/hermes-api-port.ts`:
- Around line 125-154: Update the missing-or-invalid registry-state path used by
restore_onboard_forward_after_post_checks so it reuses the persisted result from
resolveOnboardHermesApiPort instead of falling back to the fixed 8642 port. If
no valid resolved port is available, fail closed rather than starting a watcher
on the default port, and add coverage for a custom hermesApiPort followed by
invalid registry state.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Enterprise
Run ID: e00f63d3-7c15-4071-a6d8-692b6001f10a
📒 Files selected for processing (37)
agents/hermes/mcp-config-transaction.pyagents/hermes/plugin/__init__.pyagents/hermes/start.shci/source-architecture-budget.jsondocs/deployment/deploy-to-headless-server.mdxdocs/deployment/set-up-mcp-bridge.mdxdocs/get-started/quickstart-hermes.mdxdocs/reference/commands.mdxdocs/reference/troubleshooting.mdxscripts/install.shscripts/managed-gateway-control.pysrc/commands/sandbox/agent.tssrc/lib/actions/sandbox/agent/passthrough-help.tssrc/lib/actions/sandbox/agent/passthrough.test.tssrc/lib/actions/sandbox/agent/passthrough.tssrc/lib/actions/sandbox/forward-recovery-declared-ports.test.tssrc/lib/actions/sandbox/forward-recovery.tssrc/lib/actions/sandbox/process-recovery.tssrc/lib/actions/sandbox/snapshot-restore-clone-ports.test.tssrc/lib/actions/sandbox/snapshot.tssrc/lib/agent/onboard.test.tssrc/lib/agent/onboard.tssrc/lib/core/ports.tssrc/lib/onboard/agent-dashboard-forward.tssrc/lib/onboard/dashboard-port.tssrc/lib/onboard/hermes-api-port.test.tssrc/lib/onboard/hermes-api-port.tssrc/lib/onboard/hermes-dashboard.tssrc/lib/onboard/managed-startup-profile.test.tssrc/lib/onboard/managed-startup/profile.tssrc/lib/onboard/preflight-ports.tssrc/lib/onboard/sandbox-create-launch.tssrc/lib/onboard/sandbox-registration.tssrc/lib/state/gateway-registry.tssrc/lib/state/registry.tssrc/lib/state/registry/types.tstest/hermes-mcp-reload-convergence.test.ts
🚧 Files skipped from review as they are similar to previous changes (35)
- src/commands/sandbox/agent.ts
- src/lib/core/ports.ts
- src/lib/state/registry.ts
- src/lib/actions/sandbox/forward-recovery-declared-ports.test.ts
- docs/deployment/set-up-mcp-bridge.mdx
- docs/deployment/deploy-to-headless-server.mdx
- ci/source-architecture-budget.json
- src/lib/agent/onboard.test.ts
- src/lib/onboard/managed-startup-profile.test.ts
- src/lib/onboard/sandbox-registration.ts
- src/lib/onboard/preflight-ports.ts
- docs/get-started/quickstart-hermes.mdx
- src/lib/actions/sandbox/snapshot.ts
- scripts/managed-gateway-control.py
- src/lib/state/gateway-registry.ts
- src/lib/state/registry/types.ts
- src/lib/actions/sandbox/agent/passthrough.test.ts
- src/lib/actions/sandbox/process-recovery.ts
- docs/reference/troubleshooting.mdx
- src/lib/actions/sandbox/agent/passthrough.ts
- src/lib/actions/sandbox/snapshot-restore-clone-ports.test.ts
- src/lib/onboard/managed-startup/profile.ts
- src/lib/onboard/hermes-dashboard.ts
- src/lib/onboard/hermes-api-port.test.ts
- scripts/install.sh
- src/lib/actions/sandbox/forward-recovery.ts
- src/lib/actions/sandbox/agent/passthrough-help.ts
- src/lib/onboard/agent-dashboard-forward.ts
- agents/hermes/mcp-config-transaction.py
- agents/hermes/start.sh
- docs/reference/commands.mdx
- test/hermes-mcp-reload-convergence.test.ts
- src/lib/onboard/dashboard-port.ts
- src/lib/agent/onboard.ts
- src/lib/onboard/sandbox-create-launch.ts
The profile module is bundled into the managed image and executed directly by node type stripping, so a relative value import broke both consumers. Declare the Hermes API port range locally and pin it to core/ports through the profile test. Signed-off-by: Tinson Lai <tinsonl@nvidia.com>
Signed-off-by: Tinson Lai <tinsonl@nvidia.com>
The non-root startup path returned before the root-only marker write, so the same-uid helpers that probe the public relay fell back to the default port. It now publishes the marker with the identity that runs the gateway and refuses to start when an allocated port cannot be published. Documentation states the per-sandbox port where a page still named the default. Signed-off-by: Tinson Lai <tinsonl@nvidia.com>
Rerunning onboarding with NEMOCLAW_HERMES_API_PORT and without --recreate-sandbox moves the host forward while the relay keeps the recorded port, which is the opposite of what the reference stated. Signed-off-by: Tinson Lai <tinsonl@nvidia.com>
… into fix/hermes-api-port-per-sandbox
There was a problem hiding this comment.
Actionable comments posted: 5
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@agents/hermes/start.sh`:
- Around line 2915-2929: Update both current-user and root API-port marker
publication paths, including publish_hermes_api_port_marker_current_user and the
corresponding root flow, to write the value to a temporary file, apply final
permissions (and root ownership where applicable), then atomically rename it
into place. Remove the successful default-port fallback when cleanup or
replacement fails: if an old marker remains or publication cannot complete, fail
closed with a nonzero result. Add restart coverage for stale markers and
publication failures.
- Around line 3342-3344: Harden the marker publication around
HERMES_API_PORT_MARKER by validating that its parent directory is a real
root-owned, non-writable directory before writing. Replace the direct printf
redirection with a root-created temporary file in that directory, set its
contents and permissions, then atomically rename it to the marker so planted
symlinks cannot be followed; add a negative test covering a pre-existing marker
symlink.
In `@docs/security/credential-rotation.mdx`:
- Around line 219-221: Update the credential-rotation commands to use the
placeholder <api-port> instead of the literal 8642, including forwarding and API
probe commands. Explicitly instruct users to replace <api-port> with the port
shown for their selected <sandbox> by openshell forward list, while preserving
the existing first-sandbox context.
In `@src/lib/onboard/managed-startup-profile.test.ts`:
- Around line 978-986: Update the test around validateManagedStartupProfile to
iterate through every port from HERMES_API_PORT_RANGE_START through
HERMES_API_PORT_RANGE_END inclusively, asserting each reserved port throws.
Preserve or add adjacent-port assertions to confirm ports immediately outside
the declared range remain accepted.
In `@test/hermes-api-port-marker.test.ts`:
- Around line 15-33: Remove both conditionals from
runHermesApiPortMarkerPublication by extracting the runtime-parent and
stale-marker setup variants into named helpers, such as writable, blocked, and
read-only-stale-marker setups. Update the runner to accept and invoke the
selected setup helper so its body remains linear while preserving the existing
test scenarios.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Enterprise
Run ID: 0f741197-7e5c-4cb9-8f95-03fa38a1feb2
📒 Files selected for processing (19)
agents/hermes/mcp-config-transaction.pyagents/hermes/plugin/__init__.pyagents/hermes/start.shci/source-architecture-budget.jsondocs/get-started/quickstart-hermes.mdxdocs/manage-sandboxes/run-sandboxes.mdxdocs/reference/commands.mdxdocs/reference/troubleshooting.mdxdocs/security/credential-rotation.mdxscripts/managed-gateway-control.pysrc/lib/actions/sandbox/agent/passthrough-help.tssrc/lib/actions/sandbox/forward-recovery-declared-ports.test.tssrc/lib/agent/onboard.test.tssrc/lib/agent/onboard.tssrc/lib/onboard/managed-startup-profile.test.tssrc/lib/onboard/managed-startup/profile.tstest/hermes-api-port-marker.test.tstest/mcp-tool-discovery-image-contract.test.tstools/mcp-tool-discovery-runtime/reviewed-runtime-bundle/managed-startup-image-runtime.bundle
🚧 Files skipped from review as they are similar to previous changes (12)
- docs/get-started/quickstart-hermes.mdx
- src/lib/actions/sandbox/agent/passthrough-help.ts
- scripts/managed-gateway-control.py
- src/lib/actions/sandbox/forward-recovery-declared-ports.test.ts
- docs/reference/troubleshooting.mdx
- agents/hermes/plugin/init.py
- src/lib/agent/onboard.ts
- src/lib/agent/onboard.test.ts
- src/lib/onboard/managed-startup/profile.ts
- ci/source-architecture-budget.json
- agents/hermes/mcp-config-transaction.py
- docs/reference/commands.mdx
Signed-off-by: Carlos Villela <cvillela@nvidia.com>
Signed-off-by: Carlos Villela <cvillela@nvidia.com>
There was a problem hiding this comment.
Actionable comments posted: 6
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
test/managed-gateway-control.test.ts (1)
148-157: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick winAdd rejection coverage for the new
_hermes_api_portguard.The fixture now supplies a valid
NEMOCLAW_HERMES_API_PORT=8645, and Line 1306 asserts the accepted value. No case supplies a rejected value._hermes_api_portinscripts/managed-gateway-control.pyraisesControlError("GATEWAY_UNSAFE_CONFIG_PATH")for a non-numeric value and for a value outside 8642–8652. That guard exists to reject a tampered supervisor environment, and this file is its only test.Add a case that rewrites the pid 40 environment with an out-of-range value, calls
control._agent_spec("hermes", reader, supervisor), and records the resulting error code.As per coding guidelines: "Security-sensitive paths, including credential sanitization and SSRF validation, require extra test coverage."
🧪 Proposed additional case
hermes = control._agent_spec("hermes", reader, supervisor) + remove_process(proc_root, 40) + write_process( + proc_root, + namespace_path, + 40, + 222, + 1, + 1000, + b"bash\0/usr/local/bin/nemoclaw-start\0", + b"PATH=/usr/bin\0NEMOCLAW_DASHBOARD_PORT=18789\0NEMOCLAW_HERMES_API_PORT=9000\0", + ) + try: + control._agent_spec("hermes", reader, control._discover_supervisor(reader)) + unsafe_api_port = "accepted" + except control.ControlError as error: + unsafe_api_port = error.codeRestore the pid 40 fixture with the valid value afterwards, then add
unsafe_api_portto the printed object and assert"GATEWAY_UNSAFE_CONFIG_PATH".🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@test/managed-gateway-control.test.ts` around lines 148 - 157, Extend the test around the existing pid 40 fixture and control._agent_spec("hermes", reader, supervisor) call to rewrite NEMOCLAW_HERMES_API_PORT with an out-of-range value, capture the resulting ControlError code as unsafe_api_port, and include it in the printed object with an assertion for "GATEWAY_UNSAFE_CONFIG_PATH". Restore the valid 8645 environment fixture afterward so existing accepted-value coverage remains unchanged.Source: Coding guidelines
🧹 Nitpick comments (2)
agents/hermes/mcp-config-transaction.py (1)
102-112: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winThe Hermes API port range is duplicated as bare literals across the Python runtime. No Python module names the range or the default. Each site repeats
8642and8652inline, so a future range change must be found and applied in every copy. A third copy exists inagents/hermes/plugin/__init__.py_hermes_api_port, which repeats the same two literals four times. The stack outline places the authoritative contract insrc/lib/core/ports.ts, and no Python counterpart mirrors it.Introduce named module constants, for example
HERMES_API_PORT_MIN,HERMES_API_PORT_MAX, andHERMES_DEFAULT_API_PORT, and reference them at each site.
agents/hermes/mcp-config-transaction.py#L102-L112: replace the inline8642 <= port <= 8652bound in_parse_gateway_public_portwith the named constants, and define those constants next toGATEWAY_INTERNAL_PORTat Line 99.agents/hermes/mcp-config-transaction.py#L1080-L1083: replace the barereturn 8642default in_service_manager_gateway_public_portwith the named default constant, and use the same constant for the module-levelGATEWAY_PUBLIC_PORTinitializer at Line 182.scripts/managed-gateway-control.py#L1136-L1146: replace the barereturn 8642and theport < 8642 or port > 8652bound in_hermes_api_portwith equivalent named constants defined in that script.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@agents/hermes/mcp-config-transaction.py` around lines 102 - 112, Define shared-named Hermes API port constants next to GATEWAY_INTERNAL_PORT in agents/hermes/mcp-config-transaction.py, and use them in _parse_gateway_public_port, _service_manager_gateway_public_port, and the GATEWAY_PUBLIC_PORT initializer; update agents/hermes/mcp-config-transaction.py lines 102-112 and 1080-1083 accordingly. Define equivalent constants in scripts/managed-gateway-control.py and replace the literals in _hermes_api_port at lines 1136-1146, preserving the existing range validation and default behavior.test/hermes-api-port-startup.test.ts (1)
15-24: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueFail fast when a slice anchor is missing.
source.indexOf(...)returns-1when either anchor text changes inagents/hermes/start.sh.source.slice(start, end)then produces a misleading fragment instead of an explicit error, and the failure surfaces as an unrelated assertion mismatch. The repository already providesextractShellFunctionintest/support/hermes-shell-harness.ts, which throws when the pattern does not match.test/hermes-api-port-marker.test.tsuses it.Either reuse that helper or assert both offsets before slicing.
♻️ Proposed guard
const start = source.indexOf('NEMOCLAW_CMD=("$@")'); const end = source.indexOf('\nHERMES="$(command -v hermes)"', start); + if (start < 0 || end < 0) { + throw new Error("Expected the API port bootstrap block in agents/hermes/start.sh"); + }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@test/hermes-api-port-startup.test.ts` around lines 15 - 24, Make the startup-script extraction fail explicitly when either anchor is missing: update the setup around NEMOCLAW_CMD and HERMES anchor lookup to reuse extractShellFunction from the Hermes shell harness, or validate both indexOf results before calling source.slice. Preserve the existing extracted script behavior when both anchors are found.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/lib/actions/uninstall/hermes-forward-watcher-installer.test.ts`:
- Around line 79-116: Update the parameterized cases in the resolver test:
rename the existing string-port case to “non-number port,” and add a separate
fractional numeric port case using hermesApiPort 8645.5 with status 1 and empty
output. Keep the test focused on externally observable resolution behavior.
In `@test/hermes-api-port-marker.test.ts`:
- Line 27: Add a negative-test setup helper alongside trustedRuntimePrelude that
makes stat() return an untrusted ownership or mode value, then add a test
exercising prepare_hermes_root_runtime_dir with that prelude. Assert the script
exits non-zero and no marker is published, while preserving the existing
trusted-prelude tests.
In `@test/hermes-api-port-startup.test.ts`:
- Around line 44-62: Add test cases in the “agents/hermes/start.sh API port
allocation” suite for inclusive valid boundaries 8642 and 8652, plus malformed
inputs including a non-numeric value, an empty value, and surrounding
whitespace. Assert each malformed or out-of-range input exits with status 1 and
reports “Invalid NEMOCLAW_HERMES_API_PORT”, while preserving the existing
interior-port success coverage.
In `@test/hermes-mcp-api-port.test.ts`:
- Around line 26-53: Extend the test around _service_manager_gateway_public_port
with an absent-variable case asserting the default port 8642, and an
identity_change case that makes _gateway_identity return a different identity
during revalidation and asserts PermissionError with the “Hermes service-manager
identity changed while reading” message. Include both results in the test’s
collected output and expected object, while preserving the existing accepted and
rejected cases.
In `@test/hermes-mcp-config-transaction.test.ts`:
- Line 1409: Add a test scenario in the probe-related coverage that does not
stub out `_configure_gateway_public_port`, allowing it to raise
`PermissionError` when the port cannot be resolved; assert that `probe()`
propagates the failure and `main` returns exit code 2 with the sanitized error
path, while keeping the existing mock only in scenarios that require successful
port configuration.
In `@test/hermes-plugin-handlers.test.ts`:
- Around line 42-51: Extend the test around _hermes_api_port by replacing the
always-failing open stub with a marker-aware stub for additional cases. Clear
NEMOCLAW_HERMES_API_PORT, return marker contents for 8646, 9000, and not-a-port,
collect marker_results, and assert the accepted value plus 8642 fallback results
as specified while preserving the existing unreadable-marker coverage.
---
Outside diff comments:
In `@test/managed-gateway-control.test.ts`:
- Around line 148-157: Extend the test around the existing pid 40 fixture and
control._agent_spec("hermes", reader, supervisor) call to rewrite
NEMOCLAW_HERMES_API_PORT with an out-of-range value, capture the resulting
ControlError code as unsafe_api_port, and include it in the printed object with
an assertion for "GATEWAY_UNSAFE_CONFIG_PATH". Restore the valid 8645
environment fixture afterward so existing accepted-value coverage remains
unchanged.
---
Nitpick comments:
In `@agents/hermes/mcp-config-transaction.py`:
- Around line 102-112: Define shared-named Hermes API port constants next to
GATEWAY_INTERNAL_PORT in agents/hermes/mcp-config-transaction.py, and use them
in _parse_gateway_public_port, _service_manager_gateway_public_port, and the
GATEWAY_PUBLIC_PORT initializer; update agents/hermes/mcp-config-transaction.py
lines 102-112 and 1080-1083 accordingly. Define equivalent constants in
scripts/managed-gateway-control.py and replace the literals in _hermes_api_port
at lines 1136-1146, preserving the existing range validation and default
behavior.
In `@test/hermes-api-port-startup.test.ts`:
- Around line 15-24: Make the startup-script extraction fail explicitly when
either anchor is missing: update the setup around NEMOCLAW_CMD and HERMES anchor
lookup to reuse extractShellFunction from the Hermes shell harness, or validate
both indexOf results before calling source.slice. Preserve the existing
extracted script behavior when both anchors are found.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Enterprise
Run ID: 03e2ae30-a1cf-4cea-9ad3-8bb9beb8b5c3
📒 Files selected for processing (19)
agents/hermes/mcp-config-transaction.pyagents/hermes/plugin/__init__.pyagents/hermes/start.shdocs/reference/commands.mdxdocs/security/credential-rotation.mdxscripts/install.shscripts/managed-gateway-control.pysrc/lib/actions/uninstall/hermes-forward-watcher-installer.test.tssrc/lib/onboard/hermes-api-port.test.tssrc/lib/onboard/hermes-api-port.tssrc/lib/onboard/managed-startup-profile.test.tssrc/lib/onboard/sandbox-create-launch.tssrc/lib/onboard/sandbox-registration.tstest/hermes-api-port-marker.test.tstest/hermes-api-port-startup.test.tstest/hermes-mcp-api-port.test.tstest/hermes-mcp-config-transaction.test.tstest/hermes-plugin-handlers.test.tstest/managed-gateway-control.test.ts
🚧 Files skipped from review as they are similar to previous changes (9)
- src/lib/onboard/managed-startup-profile.test.ts
- src/lib/onboard/sandbox-registration.ts
- agents/hermes/plugin/init.py
- src/lib/onboard/hermes-api-port.test.ts
- scripts/install.sh
- src/lib/onboard/hermes-api-port.ts
- agents/hermes/start.sh
- docs/reference/commands.mdx
- docs/security/credential-rotation.mdx
| module.os.lstat = lambda path: (_ for _ in ()).throw(FileNotFoundError(path)) | ||
| module._gateway_identity = lambda: (123, 456) | ||
| module._gateway_has_managed_parent = lambda pid: True | ||
| module._configure_gateway_public_port = lambda: None |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
Add a case where probe() cannot resolve the port.
This stub removes the only new behavior that probe() gained. probe() now calls _configure_gateway_public_port, which raises PermissionError when the root marker is unavailable or the service-manager environment is unusable. No test proves that probe propagates that failure and that main returns exit code 2.
Add one scenario that leaves _configure_gateway_public_port raising and asserts the sanitized error path.
As per path instructions for **/*.test.{ts,js,mts,mjs,cts,cjs}: "Flag ... broad mocks that bypass the behavior under test."
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@test/hermes-mcp-config-transaction.test.ts` at line 1409, Add a test scenario
in the probe-related coverage that does not stub out
`_configure_gateway_public_port`, allowing it to raise `PermissionError` when
the port cannot be resolved; assert that `probe()` propagates the failure and
`main` returns exit code 2 with the sanitized error path, while keeping the
existing mock only in scenarios that require successful port configuration.
Source: Path instructions
Signed-off-by: Carlos Villela <cvillela@nvidia.com>
A recreate that is not resuming an interrupted session removes the registry row first, so it already allocates a free port and destroy is not required. The conflict error also named a recorded port that a legacy sandbox does not have. Signed-off-by: Tinson Lai <tinsonl@nvidia.com>
Signed-off-by: Carlos Villela <cvillela@nvidia.com>
Signed-off-by: Carlos Villela <cvillela@nvidia.com>
… into fix/hermes-api-port-per-sandbox
… into fix/hermes-api-port-per-sandbox
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (6)
scripts/checks/vitest-project-overlap.mts (1)
46-46: 📐 Maintainability & Code Quality | 🔵 Trivial | 🏗️ Heavy liftUse one authoritative installer-test inventory.
The checker and Vitest configuration maintain the same membership manually. This requires synchronized edits and can allow project ownership to drift.
scripts/checks/vitest-project-overlap.mts#L46-L46: DeriveINSTALLER_INTEGRATION_TESTSfrom the canonical inventory.vitest.config.ts#L161-L161: Generate the generalintegrationexclusion from the same inventory.vitest.config.ts#L192-L192: Generate theinstaller-integrationinclusion from the same inventory.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@scripts/checks/vitest-project-overlap.mts` at line 46, Use one canonical installer-test inventory and derive all project ownership lists from it. In scripts/checks/vitest-project-overlap.mts:46-46, define INSTALLER_INTEGRATION_TESTS from that inventory; in vitest.config.ts:161-161, generate the general integration exclusion from the same inventory; and in vitest.config.ts:192-192, generate the installer-integration inclusion from it, removing duplicated manually maintained membership.Sources: Coding guidelines, Path instructions
test/hermes-api-port-marker.test.ts (1)
39-50: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick winCover the mode half of the runtime-directory guard.
setupUntrustedRuntimereports1000:1000:755. It exercises only the ownership half of the assertion "must be root-owned with mode 0755". A regression that drops the mode comparison and keeps the owner comparison still passes this file. Add a setup helper that reports0:0:777and assert the same non-zero exit and absent marker.As per path instructions for
agents/**: "Require negative-path tests that prove the boundary rejects bypasses."🛡️ Proposed additional helper and test
+function setupWorldWritableRuntime( + _runtimeParent: string, + runtimeDir: string, + _markerPath: string, + _targetPath: string, +): MarkerSetup { + fs.mkdirSync(runtimeDir, { recursive: true }); + return { + shellPrelude: ['stat() { printf "%s\\n" "0:0:777"; }', "chown() { return 0; }"], + targetPath: null, + }; +}+ it("refuses a root-owned runtime directory with a permissive mode (`#8543`)", () => { + const run = runHermesApiPortMarkerPublication(8645, setupWorldWritableRuntime); + + expect(run.result.status).toBe(1); + expect(run.result.stderr).toContain("must be root-owned with mode 0755"); + expect(run.marker).toBeNull(); + });🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@test/hermes-api-port-marker.test.ts` around lines 39 - 50, Add a separate untrusted-runtime setup helper that reports root ownership with mode 0777, then add a negative-path test using it. Assert the command exits non-zero and the marker remains absent, covering the mode check independently of the existing setupUntrustedRuntime ownership case.Source: Path instructions
agents/hermes/mcp-config-transaction.py (1)
1080-1083: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick winConsider rejecting an empty
NEMOCLAW_HERMES_API_PORTinstead of defaulting.Line 1082 treats an absent variable and a present-but-empty variable the same way and returns
8642. An absent variable is the legacy case. A present-but-empty variable indicates a broken launch and currently resolves to a port that another sandbox may own. Splitting the two conditions keeps the legacy default and fails closed on the malformed case.♻️ Proposed change
- if not values or not values[0]: + if not values: return 8642 + if not values[0]: + raise PermissionError("Hermes service-manager API port is malformed")🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@agents/hermes/mcp-config-transaction.py` around lines 1080 - 1083, Update the port-resolution logic to distinguish an unset NEMOCLAW_HERMES_API_PORT from a present-but-empty value: preserve the 8642 legacy default only when the variable is absent, and reject an explicitly empty value with an appropriate error instead of selecting a port.test/hermes-api-port-startup.test.ts (2)
44-62: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick winNo suite exercises the inclusive edges of the 8642-8652 range. The range is re-implemented in the shell startup path, the shell resolver in
scripts/install.sh, and the Python resolver. Each suite tests one interior port and one far-outside port, so an off-by-one in any single implementation stays undetected.
test/hermes-api-port-startup.test.ts#L44-L62: add 8642 and 8652 as accepted ports, and add the malformed class of non-numeric, empty, and whitespace-padded values to the rejected cases.src/lib/actions/uninstall/hermes-forward-watcher-installer.test.ts#L86-L91: add registry cases forhermesApiPort8642 and 8652 with status 0, and cases for 8641 and 8653 with status 1.As per path instructions for
agents/**: "Require negative-path tests that prove the boundary rejects bypasses."🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@test/hermes-api-port-startup.test.ts` around lines 44 - 62, Expand test/hermes-api-port-startup.test.ts#L44-L62 to accept boundary ports 8642 and 8652 and reject non-numeric, empty, and whitespace-padded values. In src/lib/actions/uninstall/hermes-forward-watcher-installer.test.ts#L86-L91, add registry cases accepting 8642 and 8652 with status 0 and rejecting 8641 and 8653 with status 1, covering both inclusive boundaries and their bypasses.Source: Path instructions
16-24: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winFail loudly when the slice anchors are not found.
indexOfreturns-1whenagents/hermes/start.shno longer containsNEMOCLAW_CMD=("$@")or theHERMES=line.source.slice(-1, end)then yields an almost empty fragment, and the generated script fails with an unrelated bash error such as an unboundPUBLIC_PORT. The sibling filetest/hermes-api-port-marker.test.tsusesextractShellFunctionfromtest/support/hermes-shell-harness.ts, which throws a named error instead. Reuse that helper, or assert both indices before slicing.♻️ Proposed guard
const start = source.indexOf('NEMOCLAW_CMD=("$@")'); const end = source.indexOf('\nHERMES="$(command -v hermes)"', start); + if (start < 0 || end < 0) { + throw new Error("Expected the API port bootstrap block in agents/hermes/start.sh"); + }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@test/hermes-api-port-startup.test.ts` around lines 16 - 24, Update the test setup around the source.slice call to fail immediately when either NEMOCLAW_CMD=("$@") or the HERMES command anchor is missing. Prefer reusing extractShellFunction from hermes-shell-harness.ts as in hermes-api-port-marker.test.ts; otherwise assert both indexOf results are non-negative before slicing and report the missing anchor clearly.test/hermes-mcp-api-port.test.ts (1)
33-38: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick winCover
_read_service_manager_environmentinstead of always replacing it.Every case replaces
_read_service_manager_environment. The real function enforcesMAX_SERVICE_MANAGER_ENVIRONMENT_BYTESand converts a missing/proc/<pid>/environinto aPermissionError. Neither guard runs in this file. A regression that removes the size cap passes. Add a case that calls the real function against a temporary file path, or add a case that asserts the oversize and missing-file errors.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@test/hermes-mcp-api-port.test.ts` around lines 33 - 38, Extend the tests around _read_service_manager_environment to exercise the real implementation rather than only the monkeypatched replacement. Add coverage for the MAX_SERVICE_MANAGER_ENVIRONMENT_BYTES limit and the PermissionError raised when the environment source is missing, using a temporary file path or equivalent fixture; keep the existing gateway-port cases unchanged.Source: Path instructions
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@test/hermes-api-port-marker.test.ts`:
- Around line 129-136: Update the fixture execution helper around the result,
marker, mode, and target reads to perform all captures inside a try block, and
move tmpDir cleanup into a finally block so fs operations cannot leak the
temporary directory. Preserve the returned result, marker, mode, and target
values on successful execution.
In `@test/hermes-mcp-api-port.test.ts`:
- Around line 14-15: Add Python tests for _root_gateway_public_port_marker and
_resolve_gateway_public_port, covering marker security validation, precedence
over other port sources, and the root-level error raised when no marker exists.
Keep the tests focused on these behaviors and align them with the existing
Hermes MCP port-resolution coverage.
---
Nitpick comments:
In `@agents/hermes/mcp-config-transaction.py`:
- Around line 1080-1083: Update the port-resolution logic to distinguish an
unset NEMOCLAW_HERMES_API_PORT from a present-but-empty value: preserve the 8642
legacy default only when the variable is absent, and reject an explicitly empty
value with an appropriate error instead of selecting a port.
In `@scripts/checks/vitest-project-overlap.mts`:
- Line 46: Use one canonical installer-test inventory and derive all project
ownership lists from it. In scripts/checks/vitest-project-overlap.mts:46-46,
define INSTALLER_INTEGRATION_TESTS from that inventory; in
vitest.config.ts:161-161, generate the general integration exclusion from the
same inventory; and in vitest.config.ts:192-192, generate the
installer-integration inclusion from it, removing duplicated manually maintained
membership.
In `@test/hermes-api-port-marker.test.ts`:
- Around line 39-50: Add a separate untrusted-runtime setup helper that reports
root ownership with mode 0777, then add a negative-path test using it. Assert
the command exits non-zero and the marker remains absent, covering the mode
check independently of the existing setupUntrustedRuntime ownership case.
In `@test/hermes-api-port-startup.test.ts`:
- Around line 44-62: Expand test/hermes-api-port-startup.test.ts#L44-L62 to
accept boundary ports 8642 and 8652 and reject non-numeric, empty, and
whitespace-padded values. In
src/lib/actions/uninstall/hermes-forward-watcher-installer.test.ts#L86-L91, add
registry cases accepting 8642 and 8652 with status 0 and rejecting 8641 and 8653
with status 1, covering both inclusive boundaries and their bypasses.
- Around line 16-24: Update the test setup around the source.slice call to fail
immediately when either NEMOCLAW_CMD=("$@") or the HERMES command anchor is
missing. Prefer reusing extractShellFunction from hermes-shell-harness.ts as in
hermes-api-port-marker.test.ts; otherwise assert both indexOf results are
non-negative before slicing and report the missing anchor clearly.
In `@test/hermes-mcp-api-port.test.ts`:
- Around line 33-38: Extend the tests around _read_service_manager_environment
to exercise the real implementation rather than only the monkeypatched
replacement. Add coverage for the MAX_SERVICE_MANAGER_ENVIRONMENT_BYTES limit
and the PermissionError raised when the environment source is missing, using a
temporary file path or equivalent fixture; keep the existing gateway-port cases
unchanged.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Enterprise
Run ID: 50cfb5f3-efcf-4450-b180-af2cbc7818cb
📒 Files selected for processing (26)
agents/hermes/mcp-config-transaction.pyagents/hermes/plugin/__init__.pyagents/hermes/start.shci/test-file-size-budget.jsondocs/reference/commands.mdxdocs/reference/troubleshooting.mdxdocs/security/credential-rotation.mdxscripts/checks/vitest-project-overlap.mtsscripts/install.shscripts/managed-gateway-control.pysrc/lib/actions/uninstall/hermes-forward-watcher-installer.test.tssrc/lib/onboard/hermes-api-port.test.tssrc/lib/onboard/hermes-api-port.tssrc/lib/onboard/managed-startup-profile.test.tssrc/lib/onboard/sandbox-create-launch.tssrc/lib/onboard/sandbox-registration.tstest/hermes-api-port-marker.test.tstest/hermes-api-port-startup.test.tstest/hermes-mcp-api-port.test.tstest/hermes-mcp-config-transaction.test.tstest/hermes-mcp-probe-api-port.test.tstest/hermes-plugin-handlers.test.tstest/install-hermes-forward-restore.test.tstest/install-preflight.test.tstest/managed-gateway-control.test.tsvitest.config.ts
🚧 Files skipped from review as they are similar to previous changes (14)
- docs/security/credential-rotation.mdx
- test/hermes-plugin-handlers.test.ts
- src/lib/onboard/hermes-api-port.test.ts
- scripts/install.sh
- agents/hermes/start.sh
- src/lib/onboard/hermes-api-port.ts
- scripts/managed-gateway-control.py
- test/managed-gateway-control.test.ts
- src/lib/onboard/managed-startup-profile.test.ts
- src/lib/onboard/sandbox-registration.ts
- agents/hermes/plugin/init.py
- docs/reference/commands.mdx
- src/lib/onboard/sandbox-create-launch.ts
- docs/reference/troubleshooting.mdx
Signed-off-by: Tinson Lai <tinsonl@nvidia.com>
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@test/hermes-api-port-marker.test.ts`:
- Around line 129-137: Move the try block in the fixture helper to immediately
follow mkdtempSync, enclosing setup, script creation, spawnSync execution,
result capture, and cleanup. Keep the existing finally block with fs.rmSync so
any failure in setup or execution still removes tmpDir.
In `@test/hermes-mcp-api-port.test.ts`:
- Around line 110-135: Make the regular-file cases in the loop invoking
_root_gateway_public_port_marker independent of the test process UID by mocking
os.fstat to retain actual metadata while reporting st_uid and st_gid as 0,
allowing mode, hard-link, and size validation to execute. Leave the symlink case
unchanged so O_NOFOLLOW continues rejecting it before metadata validation.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Enterprise
Run ID: 018f5f00-0e57-4298-bc04-c8f74325d6a6
📒 Files selected for processing (7)
docs/reference/commands.mdxdocs/reference/troubleshooting.mdxsrc/lib/actions/sandbox/forward-recovery.tstest/hermes-api-port-marker.test.tstest/hermes-api-port-startup.test.tstest/hermes-mcp-api-port.test.tstest/hermes-plugin-handlers.test.ts
🚧 Files skipped from review as they are similar to previous changes (5)
- docs/reference/troubleshooting.mdx
- src/lib/actions/sandbox/forward-recovery.ts
- docs/reference/commands.mdx
- test/hermes-api-port-startup.test.ts
- test/hermes-plugin-handlers.test.ts
Signed-off-by: Tinson Lai <tinsonl@nvidia.com>
Signed-off-by: Tinson Lai <tinsonl@nvidia.com>
There was a problem hiding this comment.
🧹 Nitpick comments (1)
src/lib/onboard/agent-dashboard-forward.test.ts (1)
41-70: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick winMake the regression assertion independent of forwarding order.
The
toHaveBeenNthCalledWithassertions couple this test to the current forwarding order and array order. The negativetoHaveBeenCalledWith(..., expect.anything())assertion can miss a default-port call when the call has no third argument. Capture the forwarded URLs, assert that8643is present and8642is absent, and useexpect.objectContainingfor the allocated-port options.Proposed diff
+ const forwardedUrls: string[] = []; const ensureDashboardForward = vi.fn((_sandboxName, chatUiUrl = "http://127.0.0.1:18789") => { + forwardedUrls.push(chatUiUrl); const parsed = new URL(chatUiUrl); return Number(parsed.port); }); ... - expect(ensureDashboardForward).toHaveBeenNthCalledWith(1, "hm", "http://127.0.0.1:18789", { - preserveSandboxPorts: [18789, 8643, 3978], - }); - expect(ensureDashboardForward).toHaveBeenNthCalledWith(2, "hm", "http://127.0.0.1:8643", { - preserveSandboxPorts: [18789, 8643, 3978], - allowPortReallocation: false, - }); - expect(ensureDashboardForward).not.toHaveBeenCalledWith( - "hm", - "http://127.0.0.1:8642", - expect.anything(), + expect(forwardedUrls).toContain("http://127.0.0.1:8643"); + expect(forwardedUrls).not.toContain("http://127.0.0.1:8642"); + expect(ensureDashboardForward).toHaveBeenCalledWith( + "hm", + "http://127.0.0.1:8643", + expect.objectContaining({ + preserveSandboxPorts: expect.arrayContaining([18789, 8643, 3978]), + allowPortReallocation: false, + }), );As per path instructions: Review tests for behavioral confidence rather than implementation lock-in, and prefer observable outcomes through the public boundary over mock-call assertions.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/lib/onboard/agent-dashboard-forward.test.ts` around lines 41 - 70, Update the assertions around ensureAgentDashboardForward and ensureDashboardForward to capture forwarded URLs from the mock calls, then assert that port 8643 is present and 8642 is absent without relying on call order. Match allocation options with expect.objectContaining, and ensure the absence check also covers calls without a third argument.Source: Path instructions
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@src/lib/onboard/agent-dashboard-forward.test.ts`:
- Around line 41-70: Update the assertions around ensureAgentDashboardForward
and ensureDashboardForward to capture forwarded URLs from the mock calls, then
assert that port 8643 is present and 8642 is absent without relying on call
order. Match allocation options with expect.objectContaining, and ensure the
absence check also covers calls without a third argument.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Enterprise
Run ID: de93ff21-7ce4-4e4c-b80d-401587618776
📒 Files selected for processing (2)
docs/deployment/deploy-to-headless-server.mdxsrc/lib/onboard/agent-dashboard-forward.test.ts
🚧 Files skipped from review as they are similar to previous changes (1)
- docs/deployment/deploy-to-headless-server.mdx
Signed-off-by: Tinson Lai <tinsonl@nvidia.com>
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@test/hermes-mcp-api-port.test.ts`:
- Around line 277-280: Replace the mkdtemp-based absent-marker setup in the
probe test with a tempfile.TemporaryDirectory context, and execute module.main()
within that context so the temporary directory is removed even when SystemExit
is raised.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Enterprise
Run ID: 9ab234f4-d08a-4854-b8b7-081ff3907931
📒 Files selected for processing (2)
test/hermes-api-port-marker.test.tstest/hermes-mcp-api-port.test.ts
| module.GATEWAY_PUBLIC_PORT_PATH = str(pathlib.Path(tempfile.mkdtemp()) / "absent-marker") | ||
| module.os.geteuid = lambda: 0 | ||
| sys.argv = ["mcp-config-transaction.py", "probe"] | ||
| raise SystemExit(module.main()) |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win
Clean up the temporary directory.
Line 277 calls tempfile.mkdtemp(), but Line 280 exits the child process through SystemExit without removing the directory. Repeated test runs leave stale directories in the system temporary area. Use tempfile.TemporaryDirectory() and keep module.main() inside its context.
Proposed cleanup
- module.GATEWAY_PUBLIC_PORT_PATH = str(pathlib.Path(tempfile.mkdtemp()) / "absent-marker")
- module.os.geteuid = lambda: 0
- sys.argv = ["mcp-config-transaction.py", "probe"]
- raise SystemExit(module.main())
+ with tempfile.TemporaryDirectory() as temp_dir:
+ module.GATEWAY_PUBLIC_PORT_PATH = str(pathlib.Path(temp_dir) / "absent-marker")
+ module.os.geteuid = lambda: 0
+ sys.argv = ["mcp-config-transaction.py", "probe"]
+ raise SystemExit(module.main())📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| module.GATEWAY_PUBLIC_PORT_PATH = str(pathlib.Path(tempfile.mkdtemp()) / "absent-marker") | |
| module.os.geteuid = lambda: 0 | |
| sys.argv = ["mcp-config-transaction.py", "probe"] | |
| raise SystemExit(module.main()) | |
| with tempfile.TemporaryDirectory() as temp_dir: | |
| module.GATEWAY_PUBLIC_PORT_PATH = str(pathlib.Path(temp_dir) / "absent-marker") | |
| module.os.geteuid = lambda: 0 | |
| sys.argv = ["mcp-config-transaction.py", "probe"] | |
| raise SystemExit(module.main()) |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@test/hermes-mcp-api-port.test.ts` around lines 277 - 280, Replace the
mkdtemp-based absent-marker setup in the probe test with a
tempfile.TemporaryDirectory context, and execute module.main() within that
context so the temporary directory is removed even when SystemExit is raised.
There was a problem hiding this comment.
The commit under review is 98d4595d1.
The previous security finding is resolved. The root-separated path publishes the API port in a root-owned marker with an atomic file replacement. Helpers in the same-user-ID topology read the port from the verified service-manager environment. These helpers fail closed if they cannot verify that source.
Two blocking findings remain:
-
Correct Hermes API port selection in the headless procedure.
docs/deployment/deploy-to-headless-server.mdx:293tells the reader to use theheadless-agentrow. Hermes can have dashboard and API port-forward rows for the same sandbox. The example at line 298 then setsAPI_PORT=8642, which discards the selected value. For another Hermes sandbox, this can send the bearer token to a sibling sandbox or a non-OpenShell listener. Tell the reader to select theheadless-agentrow whose port is from8642through8652. Tell the reader to replace the example value with that port. -
Provide passing managed-runtime activation evidence for Hermes. The
PR exact all-agent managed runtime activationcheck failed during Hermes onboarding for five consecutive PR commits. The latest completed run reportsManaged bootstrap Docker supervisor did not reconnectand an indeterminate cutover state: https://github.com/NVIDIA/NemoClaw/actions/runs/31318932707/job/93261035150. GitHub Actions skipped this check for the latest PR commit because OpenClaw direct startup failed first. Concurrent PRs pass the same check, so the evidence does not identify a repository-wide failure. Correct the branch-specific failure and provide a passing result for the latest PR commit.
After you update the PR, rerun the documentation writer review. Refresh the receipt metadata because it identifies commit bf43b741e and does not cover the commit under review.
Summary
The Hermes OpenAI-compatible API port was a fixed host-wide
8642, so only one Hermes sandbox per host could own its host forward. A snapshot clone therefore received no inference forward, andgateway restarton the clone exited 1 with a forward recovery failure it could never repair. Each Hermes sandbox now allocates its own API port from8642through8652, and every consumer resolves that port instead of the manifest default.Related Issue
Fixes #8543
Changes
src/lib/core/ports.tsaddsHERMES_API_PORT_RANGE_START/ENDandisHermesApiPort.8642becomes the range start rather than a host-wide reservation.src/lib/onboard/hermes-api-port.tsis new: allocation,resolveOnboardHermesApiPort,resolveSandboxHermesApiPort, andretargetHermesApiPortInUrl. The port is published back toNEMOCLAW_HERMES_API_PORTso the sandbox-create environment, the registry row, and the host forward agree on one value without threading an argument through the onboarding entrypoint, matching howensureAgentDashboardForwardalready propagates the dashboard port throughCHAT_UI_URL. A registered sandbox keeps its recorded port: an explicit environment value is accepted only at the create, recreate, and created-sandbox registration boundaries, and every other consumer rejects a conflicting value before it mutates a host forward.isValidHermesApiPortaccepts only the allocation range, so an out-of-range operator value is refused outright.src/lib/onboard/dashboard-port.tsextractsfindAvailablePortInRangeso the dashboard and API allocators share one forward-list, registry, and host-bind view, and addsgetRegistryOccupiedHermesApiPortsfor the cross-gateway view.hermesApiPort, sanitised insrc/lib/state/registry.tsand validated fail-closed insrc/lib/state/gateway-registry.tsso a corrupt value cannot drop occupancy and hand out a colliding port.src/lib/actions/sandbox/snapshot.tsallocates the clone's own port before any destructive step, so range exhaustion aborts beforedeleteSandboxForRestoreremoves a--forcedestination, and overrides the value the source-entry spread would otherwise inherit.src/lib/actions/sandbox/forward-recovery.tsresolves manifest-declared forward ports against the sandbox's own ports. This fixes both halves of the reported failure: recovery no longer demands the manifest dashboard port from a sandbox that owns a different one, and it targets the sandbox's own API port.agents/hermes/start.shreadsNEMOCLAW_HERMES_API_PORTand publishes the resolved value as a0444marker at/run/nemoclaw/hermes-api-port, on the root-separated path and on the same-uid path that OpenShell-managed and macOS VM startups take. A one-shotopenshell sandbox execdoes not inherit the supervisor environment, soagents/hermes/mcp-config-transaction.py,agents/hermes/plugin/__init__.py, andscripts/managed-gateway-control.pyread that marker instead. Under privilege separation the marker is root-owned and the sandbox user cannot rewrite it; the transaction helper opens it withO_NOFOLLOWand checks ownership, mode, link count, and size before trusting it. In the same-uid topology the gateway already runs as the sandbox user, so the marker carries that same authority, and a sandbox that cannot publish an allocated port there refuses to start rather than leave its helpers probing a default port any process can bind.src/lib/onboard/managed-startup/profile.tsdeclares the reserved range locally instead of importingcore/ports. That module is bundled into the managed image and is also executed directly bynode --experimental-strip-types, and neither path resolves a relative value import;src/lib/onboard/managed-startup-profile.test.tspins the two declarations to the same values.tools/mcp-tool-discovery-runtime/reviewed-runtime-bundle/managed-startup-image-runtime.bundleis regenerated for that change, and its pinned digest intest/mcp-tool-discovery-image-contract.test.tsis updated.bundle:reviewed:checkreproduced the other three reviewed artefacts byte-for-byte, so only the managed-startup bundle moved.src/lib/agent/onboard.tspassessandboxNamethrough the API-kind dashboard branch, which otherwise announced the manifest default for a Hermes sandbox that owns another port.sandbox agentrejection message, the onboarding ready summary, andscripts/install.shresolve the sandbox's port instead of naming8642, and the installer restores a registered Hermes forward rather than the manifest default.src/lib/onboard/preflight-ports.ts,src/lib/onboard/hermes-dashboard.ts, and the managed-startup profile validation cover the whole range rather than the single literal.ci/source-architecture-budget.jsonraises four limits. Introducing one module adds edges tosnapshot.tsfan-out, thesrc/lib/onboardroot-file count, andcore/ports.ts/state/registry.tsfan-in. Two other increases were avoidable and were removed instead:getSandboxHealthProbeUrlmoved intoforward-recovery.ts, which already owned that dependency, andsnapshot.tsno longer importscore/ports.origin/mainto keep the branch current. The merge was clean.Type of Change
Quality Gates
0444marker at/run/nemoclaw/hermes-api-portthat three in-sandbox helpers read, root-owned under privilege separation and owned by the gateway's own user in the same-uid topology, and it widens the reserved-port set in the Shields-sealed managed-startup profile validation from one literal to a range. A maintainer should confirm the marker's trust boundary in both topologies and the reservation change before merge.Documentation Writer Review
docs-updateddocs/get-started/quickstart-hermes.mdx,docs/reference/commands.mdx,docs/reference/troubleshooting.mdx,docs/deployment/deploy-to-headless-server.mdx,docs/deployment/set-up-mcp-bridge.mdx,docs/security/credential-rotation.mdx, anddocs/manage-sandboxes/run-sandboxes.mdx, plus the changed comments, CLI strings, error strings, and test titles across the diff. The subagent reviewed the writing rules and documentation style over ten passes, seven of them on the merged tree. The early passes closed six blocking items, including asandbox agentrejection message and an onboarding ready summary that still emitted the manifest default, docs that pointed operators atstatus, which prints no port, a guard that applied the Hermes port to any agent, and unit tests that had begun reading the real sandbox registry. A later pass returneddocs-missingwith 14 findings, among them a Hermes credential-rotation procedure that still hardcoded8642, an agent-neutral port guard described as Hermes-only, an override whose failure result was unstated, and three reader docstrings that claimed a root-owned marker in a topology where the gateway itself owns it. All were applied. Two further passes corrected an override consequence that had been stated in the wrong direction and a guard that would have rejected the very--recreate-sandboxcommand its own error recommends; both corrections were traced throughsrc/lib/onboard/dashboard.ts,src/lib/onboard/agent-dashboard-forward.ts, andsrc/lib/onboard/sandbox-lifecycle.tsbefore they were accepted. The final pass reviewed the port-resolution hardening as a fresh change set and confirmed that the narrowed range leaves no page offering an out-of-range value, that a recreate which is not resuming an interrupted session removes the registry row and therefore allocates, and that the range messages agree word for word across TypeScript, shell, and the reference. Its two blocking findings are applied here. Every finding was verified against source before it was applied, and two requests to add explanatory comments were declined and recorded for maintainer review. The last full pass covered717a9c841; the commits after it change tests,vitest.config.ts, two budget entries, and the two documentation lines and one error string that pass itself requested.Verification
Signed-off-by:line and every commit appears asVerifiedin GitHubpre-commit,commit-msg, andpre-pushhooks passed, ornpm run validate:prpassed after refreshingorigin/mainwhen hooks were skipped or unavailablenpx vitest run --project cli src/lib/onboard/hermes-api-port.test.ts— 22 passed.npx vitest run --project integration test/generate-managed-startup-profile-fixture.test.ts test/mcp-tool-discovery-image-contract.test.ts test/source-architecture.test.ts— 31 passed.npx vitest run --project cli src/lib/onboard/managed-startup-profile.test.ts— 105 passed.npm run validate:pr— exit 0.npm run docs— 0 errors. CI on the preceding head reported all eightcli-test-shardsgreen and all threePR build and direct managed startupjobs passing, and the PR Review Advisor reported 0 blockers.npm testfor broad runtime/test-harness changes;npm run checkfor repo-wide validation/coverage changes — command/result: not run locally; left to CI.npm run docsbuilds without warnings (doc changes only) —npm run docsexits 0 with 0 errors and 2 warnings. Both warnings are pre-existinglearn.microsoft.comlanguage-reference links indocs/reference/troubleshooting.mdxthat this change does not touch, so the box stays clear rather than claiming a warning-free build.Signed-off-by: Tinson Lai tinsonl@nvidia.com
Summary by CodeRabbit
New Features
Bug Fixes
Documentation